package org.gap.jseed;

import static org.gap.jseed.JavaWriter.braces;
import static org.gap.jseed.JavaWriter.call;
import static org.gap.jseed.JavaWriter.createParameters;
import static org.gap.jseed.JavaWriter.getInvokingMethodCode;
import static org.gap.jseed.JavaWriter.getParameterTypes;
import static org.gap.jseed.JavaWriter.line;
import static org.gap.jseed.JavaWriter.returnCall;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

import org.gap.jseed.annotation.Advice;
import org.gap.jseed.annotation.BeanUtil;

public class MethodInjector extends AbstractInjector {
	private Map<Class<? extends Annotation>, Class<? extends InvocationHandler>> annotations;

	public MethodInjector() {
		annotations = new HashMap<Class<? extends Annotation>, Class<? extends InvocationHandler>>();
	}

	public void injectBehavior(Class<?> theInterface, CtClass abstraction, CtClass implementation) throws ClassNotFoundException, CannotCompileException, NotFoundException {
		for (CtMethod eachMethod : abstraction.getDeclaredMethods()) {
			for (Object eachAnnot : eachMethod.getAnnotations()) {
				for (Class<? extends Annotation> methodAnnotion : annotations.keySet()) {
					if (methodAnnotion.isInstance(eachAnnot)) {
						Class<? extends InvocationHandler> handler = annotations.get(methodAnnotion);
						String propertyName = BeanUtil.getPropertyName(eachMethod.getName() + "_" + handler.getSimpleName());
						injectField(propertyName, implementation, handler);
						Advice advice = methodAnnotion.getAnnotation(Advice.class);
						switch (advice.value()) {
						case METHOD:
							addMethodBody(implementation, eachMethod, propertyName);
							break;
						case BEFORE:
							CtMethod method = addCallSuper(implementation, eachMethod);
							addBeforeBody(implementation, method, propertyName);
							try {
								implementation.addMethod(method);
							} catch (Exception e) {
								// ignore
							}
						}
					}
				}
			}
		}
	}

	private CtMethod addCallSuper(CtClass implementation, CtMethod eachMethod) throws CannotCompileException, NotFoundException {
		CtMethod newMethod = null;
		try {
			newMethod = implementation.getMethod(eachMethod.getName(), null);
		} catch (Exception e) {
			String body = null;
			newMethod = new CtMethod(eachMethod, implementation, null);
			if (eachMethod.getMethodInfo().getCodeAttribute() == null) {
				body = ";";
			} else {
				body = line("super." + eachMethod.getName() + "($1)");
			}
			newMethod.setBody(body);
		}
		return newMethod;
	}

	private void addBeforeBody(CtClass implementation, CtMethod newMethod, String propertyName) throws NotFoundException, CannotCompileException {
		String body = braces(getInvokingMethodCode(newMethod, getParameterTypes(newMethod.getParameterTypes())) + line(returnCall(newMethod, callInvocationHandler(propertyName, newMethod))));
		newMethod.insertBefore(body);
	}

	private void addMethodBody(CtClass implementation, CtMethod eachMethod, String propertyName) throws CannotCompileException, NotFoundException {
		CtMethod newMethod = new CtMethod(eachMethod, implementation, null);

		newMethod.setBody(braces(getInvokingMethodCode(eachMethod, getParameterTypes(newMethod.getParameterTypes())) + line(returnCall(eachMethod, callInvocationHandler(propertyName, newMethod)))));
		implementation.addMethod(newMethod);
	}

	private String callInvocationHandler(String propertyName, CtMethod newMethod) throws NotFoundException {
		return call("invoke", propertyName, "this", "method", createParameters(newMethod.getParameterTypes().length));
	}

	public void add(Class<? extends Annotation> annotation, Class<? extends InvocationHandler> handler) {
		annotations.put(annotation, handler);
	}
}
